package com.eii.resubmit.aop.request.checker;

import com.eii.resubmit.utils.redis.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * @author eii
 * @time 2020/11/29 4:39 下午
 */
@Component
public class RedisLockHelper {

    /**
     * 如果要求比较高可以通过注入的方式分配
     */
    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);

    private static final String DELIMITER = "|";

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 获取锁（存在死锁风险） 因为tryLock其实执行了 1. set key 2. set key expire，这两步可能会出现，1成功后，连接断开；导致2没成功，此时这key 就不会失效了，死锁了
     *
     * @param lockKey lockKey
     * @param value   value
     * @param time    超时时间
     * @param unit    过期单位
     * @return true or false
     */
    public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
        return redisUtil.getRedisTemplate().execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
    }

    /**
     * 获取锁
     *
     * @param lockKey lockKey
     * @param uuid    UUID
     * @param timeout 超时时间
     * @param unit    过期单位
     * @return true or false
     */
    public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {
        RedisTemplate<String, Object> redisTemplate = redisUtil.getRedisTemplate();
        final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();

        boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);

        if (success) {
            redisTemplate.expire(lockKey, timeout, unit);
        } else {
            //这里为了处理,上面逻辑出现的死锁执行setIfAbsent后，redis断开，导致无法设置过期时间
            //setIfAbsent(k,v)这段代码是有问题的：当setIfAbsent成功之后断开连接，下面设置过期时间的代码stringRedisTemplate.expire(key,timeout); 是无法执行的
            //所以这里会判断旧key对应的value 是否已过期，双层校验，解决问题
            String oldVal = (String) redisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
            final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
            if (Long.parseLong(oldValues[0]) < System.currentTimeMillis()) {
                //当多个实例执行getAndSet，这里会进行下一步判断，只有第一个执行成功的实例oldValues会是过期的，这里就是双层校验
                return true;
            }
        }
        return success;

//      不要使用该方法 return redisTemplate.opsForValue().setIfAbsent(key, true, TIMEOUT, TIME_UNIT);因为这个并不能保证原子性，所以可能会有多实例调用该方法成功
    }

    /**
     * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
     */
    public void unlock(String lockKey, String value) {
        unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
    }

    /**
     * 延迟unlock
     * @param lockKey   key
     * @param uuid      client(最好是唯一键的)
     * @param delayTime 延迟时间
     * @param unit      时间单位
     */
    public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
        if (StringUtils.isEmpty(lockKey)) {
            return;
        }
        if (delayTime <= 0) {
            doUnlock(lockKey, uuid);
        } else {
            EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
        }
    }

    /**
     * @param lockKey key
     * @param uuid    client(最好是唯一键的)
     */
    private void doUnlock(final String lockKey, final String uuid) {
        RedisTemplate<String, Object> redisTemplate = redisUtil.getRedisTemplate();
        String val = (String)redisTemplate.opsForValue().get(lockKey);
        if(val != null){
            final String[] values = val.split(Pattern.quote(DELIMITER));
            if (values.length <= 0) {
                return;
            }
            if (uuid.equals(values[1])) {
                redisTemplate.delete(lockKey);
            }
        }
    }
}
